<?php

namespace pmvc\remoting\jsonrpc;

use pmvc\util\ClassUtil;
use pmvc\log\Log;

use ReflectionClass;
use Exception;

/**
 * A client for accessing JSON-RPC over sockets.
 *
 */
class StreamingJsonRpcClient {

	public $port;
	public $host;

	private $socket		= null;
	private $nextId  	= 99;

	/**
	 * Creates the client for the given port and hostname.
	 * @param string $host
	 * @param int $port
	 */
	public function __construct($host, $port) {
		$this->port	= $port;
		$this->host	= $host;
	}

	/**
	 * Destructor.
	 */
	public function __destruct() {
		if (isset($this->socket)) {
			fclose($this->socket);
		}
	}

	/**
	 * Creates and connects a socket.
	 * @throws Exception
	 */
	private function getSocket() {
		if (!isset($this->socket)) {
			$this->socket = fsockopen($this->host, $this->port);
			if ($this->socket===false) {
				$this->socket = null;
				throw new Exception(
					"Unable to connect to ".$this->host." on port ".$this->port);
			}
		}
		return $this->socket;
	}

	/**
	 * Invokes the given method with the given arguments
	 * on the server and returns it's value.  If {@code $returnType}
	 * is specified than an instance of the class that it names
	 * will be created passing the json object (stdClass) to it's
	 * constructor.
	 * 
	 * @param string $method the method to invoke
	 * @param Array $params the parameters (if any) to the method
	 * @param string $id the request id
	 */
	public function invoke($method, Array $params=Array(), $id=false) {

		// get the socket
		$socket = $this->getSocket();

		// make sure we have an ID
		if ($id===false) {
			$id = ++$this->nextId;
		}

		// create request
		$request = json_encode(Array(
			'jsonrpc'	=> '2.0',
			'method'	=> $method,
			'params'	=> $params,
			'id'		=> $id
		));
		Log::trace(__CLASS__, "json-rpc request: {0}", $request);

		// write the request
		fwrite($socket, $request);
		fflush($socket);

		// read the response
		$response = "";
		while (!feof($socket)) {
			$chunk = fread($socket, 1024);
			$response .= $chunk;
			if (strlen($chunk)!=1024) {
				break;
			}
		}

		// decode json response
		$response = json_decode($response);
		if ($response==NULL) {
			throw new Exception("JSON parsing error occured: ".json_last_error());

		// throw errors
		} else if (isset($response->error)) {
			$msg = 'JSON-RPC error';
			if (isset($response->error->message) && !empty($response->error->message)) {
				$msg .= ': "' . $response->error->message . '"';
			}
			$msg .= "\n";
			$msg .= 'HOST: ' . $this->host;
			$msg .= "\n";
			$msg .= 'PORT: ' . $this->port;
			$msg .= "\n";
			$msg .= 'Method: ' . $method;
			$msg .= "\n";
			$msg .= 'Arguments: ' . self::printArguments($params, 2);
			
			if (isset($response->error->code)) {
				throw new Exception($msg, intval($response->error->code));
			} else {
				throw new Exception($msg);
			}
	    }

	    // return the data
		return $response->result;
	}

	/**
	 * Printing arguments.
	 * @param $arg
	 * @param $depth
	 */
	private static function printArguments($args, $depth=1) {
		$argStrings = Array();
		foreach ($args as $arg) {
			$argStrings[] = self::printArgument($arg, $depth);
		}
		return implode($argStrings, ', ');
	}

	/**
	 * Print an argument.
	 * @param $arg
	 * @param $depth
	 */
	private static function printArgument($arg, $depth=1) {
		if ($arg === NULL) {
			return 'NULL';
		} else if (is_array($arg)) {
			if ($depth > 1) {
				return '[' . self::printArguments($arg, ($depth - 1)) . ']';
			} else {
				return 'Array';
			}
		} else if (is_object($arg)) {
			return 'Object';
		} else if (is_bool($arg)) {
			return ($arg === true) ? 'true' : 'false';
		} else if (is_string($arg)) {
			return "'$arg'";
		}
		return strval($arg);
	}
}

?>